namespace Wilco.CodeHilighter.Engine.SyntaxHighlighting
{
    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Linq;
    using System.Text.RegularExpressions;
    using System.Xml.Linq;

    /// <summary>
    /// Represents an Xml scanner.
    /// </summary>
    public class XmlScanner : ScannerBase
    {
        #region Fields

        private bool identifierEnabled;
        private XmlAttributeNameNode xmlAttributeNameNode;
        private XmlAttributeValueNode xmlAttributeValueNode;
        private XmlCommentNode xmlCommentNode;
        private XmlNamespaceNode xmlNamespaceNode;
        private XmlSpecialCharNode xmlSpecialCharNode;
        private XmlTagNode xmlTagNode;

        #endregion Fields

        #region Constructors

        /// <summary>
        /// Initializes a new instance of an <see cref="Wilco.CodeHilighter.Engine.SyntaxHighlighting.XmlScanner"/> class.
        /// </summary>
        public XmlScanner()
            : base(null, null)
        {
            //
        }

        /// <summary>
        /// Initializes a new instance of an <see cref="Wilco.CodeHilighter.Engine.SyntaxHighlighting.XmlScanner"/> class.
        /// </summary>
        /// <param name="tokenizer">The <see cref="Wilco.CodeHilighter.Engine.SyntaxHighlighting.TokenizerBase"/> which is used to tokenize the source code.</param>
        /// <param name="scannerResult">The <see cref="Wilco.CodeHilighter.Engine.SyntaxHighlighting.OccurrenceCollection"/> which will contain the scanner result.</param>
        public XmlScanner(TokenizerBase tokenizer, OccurrenceCollection scannerResult)
            : base(tokenizer, scannerResult)
        {
            this.xmlSpecialCharNode = new XmlSpecialCharNode();
            this.xmlSpecialCharNode.ForeColor = Color.Blue;
            this.xmlTagNode = new XmlTagNode();
            this.xmlTagNode.ForeColor = Color.Brown;//no support for Maroon in sl;
            this.xmlNamespaceNode = new XmlNamespaceNode();
            this.xmlNamespaceNode.ForeColor = Color.Purple;//no support for MediumVioletRed in sl;
            this.xmlCommentNode = new XmlCommentNode();
            this.xmlCommentNode.ForeColor = Color.Green;
            this.xmlAttributeNameNode = new XmlAttributeNameNode();
            this.xmlAttributeNameNode.ForeColor = Color.Red;
            this.xmlAttributeValueNode = new XmlAttributeValueNode();
            this.xmlAttributeValueNode.ForeColor = Color.Blue;
            this.SetID("XmlScanner");
        }

        #endregion Constructors

        #region Events

        /// <summary>
        /// Occurs when XML was matched.
        /// </summary>
        public event MatchEventHandler Match;

        #endregion Events

        #region Properties

        /// <summary>
        /// Gets or sets the Xml attribute name node.
        /// </summary>
        public XmlAttributeNameNode XmlAttributeNameNode
        {
            get
            {
                return this.xmlAttributeNameNode;
            }
            set
            {
                if (value != this.xmlAttributeNameNode)
                {
                    this.xmlAttributeNameNode = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the Xml attribute value node.
        /// </summary>
        public XmlAttributeValueNode XmlAttributeValueNode
        {
            get
            {
                return this.xmlAttributeValueNode;
            }
            set
            {
                if (value != this.xmlAttributeValueNode)
                {
                    this.xmlAttributeValueNode = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the Xml comment node.
        /// </summary>
        public XmlCommentNode XmlCommentNode
        {
            get
            {
                return this.xmlCommentNode;
            }
            set
            {
                if (value != this.xmlCommentNode)
                {
                    this.xmlCommentNode = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the Xml namespace node.
        /// </summary>
        public XmlNamespaceNode XmlNamespaceNode
        {
            get
            {
                return this.xmlNamespaceNode;
            }
            set
            {
                if (value != this.xmlNamespaceNode)
                {
                    this.xmlNamespaceNode = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the Xml special char node.
        /// </summary>
        public XmlSpecialCharNode XmlSpecialCharNode
        {
            get
            {
                return this.xmlSpecialCharNode;
            }
            set
            {
                if (value != this.xmlSpecialCharNode)
                {
                    this.xmlSpecialCharNode = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the Xml tag node.
        /// </summary>
        public XmlTagNode XmlTagNode
        {
            get
            {
                return this.xmlTagNode;
            }
            set
            {
                if (value != this.xmlTagNode)
                {
                    this.xmlTagNode = value;
                }
            }
        }

        #endregion Properties

        #region Methods

        /// <summary>
        /// Initializes a new instance of a <see cref="Wilco.CodeHilighter.Engine.SyntaxHighlighting.IScanner"/> implementation class.
        /// </summary>
        /// <param name="tokenizer">The <see cref="Wilco.CodeHilighter.Engine.SyntaxHighlighting.TokenizerBase"/> which is used to tokenize the source code.</param>
        /// <param name="scannerResult">The <see cref="Wilco.CodeHilighter.Engine.SyntaxHighlighting.OccurrenceCollection"/> which will contain the scanner result.</param>
        /// <returns>A new instance of a <see cref="Wilco.CodeHilighter.Engine.SyntaxHighlighting.IScanner"/> implementation class.</returns>
        public override IScanner Create(TokenizerBase tokenizer, OccurrenceCollection scannerResult)
        {
            return new XmlScanner(tokenizer, scannerResult);
        }

        /// <summary>
        /// Loads the state of the scanner.
        /// </summary>
        /// <param name="state">An <see cref="System.Object"/> that contains the state of the scanner.</param>
        public override void LoadState(object state)
        {
            XElement element = (XElement)state;

            this.LoadNode(element, this.xmlSpecialCharNode, "SpecialCharNode");
            this.LoadNode(element, this.xmlTagNode, "TagNode");
            this.LoadNode(element, this.xmlNamespaceNode, "NamespaceNode");
            this.LoadNode(element, this.xmlCommentNode, "CommentNode");
            this.LoadNode(element, this.xmlAttributeNameNode, "AttributeNameNode");
            this.LoadNode(element, this.xmlAttributeValueNode, "AttributeValueNode");
        }

        /// <summary>
        /// Resets the scanner.
        /// </summary>
        public override void Reset()
        {
            this.identifierEnabled = false;
        }

        /// <summary>
        /// Saves the current state of the scanner.
        /// </summary>
        /// <param name="container">The container which will contain the state.</param>
        /// <returns>An <see cref="System.Object"/> that contains the state of the scanner.</returns>
        public override object SaveState(object container)
        {
            //XDocument document = (XDocument)container;
            XElement element = (XElement)base.SaveState(container);

            // Save settings.
            XElement nodeRootElement = new XElement("nodes");
            element.Add(nodeRootElement);

            this.StoreNode(nodeRootElement, this.xmlSpecialCharNode, "SpecialCharNode");
            this.StoreNode(nodeRootElement, this.xmlTagNode, "TagNode");
            this.StoreNode(nodeRootElement, this.xmlNamespaceNode, "NamespaceNode");
            this.StoreNode(nodeRootElement, this.xmlCommentNode, "CommentNode");
            this.StoreNode(nodeRootElement, this.xmlAttributeNameNode, "AttributeNameNode");
            this.StoreNode(nodeRootElement, this.xmlAttributeValueNode, "AttributeValueNode");

            return element;
        }

        /// <summary>
        /// Scans a token.
        /// </summary>
        /// <remarks>
        /// An <see cref="Wilco.CodeHilighter.Engine.SyntaxHighlighting.IScanner"/> implementation will generally have a reference to a 
        /// <see cref="Wilco.CodeHilighter.Engine.SyntaxHighlighting.NodeCollection"/> which will be used to store results of a scan.
        /// </remarks>
        /// <param name="token">A token from the source code.</param>
        public override void Scan(string token)
        {
            if (!this.Enabled)
            {
                if (this.Child != null)
                {
                    this.Child.Scan(token);
                }
            }
            else
            {
                bool isMatch = false;

                // Match the closing tag, such as "</asp:Label>";
                if ((this.Tokenizer.Position + 2 <= this.Tokenizer.Source.Length) && this.Tokenizer.GetNextTokens(2) == "</")
                {
                    this.ScannerResult.Add(new Occurrence(this.Tokenizer.Position, 2, this.xmlSpecialCharNode));

                    // Match [special chars/namespace/]tag.
                    int offset = 2;
                    string ns = this.MatchNamespace(offset);
                    if (ns.Length > 0)
                        offset += ns.Length + 1;

                    string tag = this.MatchTag(offset);
                    if (tag.Length > 0)
                        offset += tag.Length;

                    // Match end character of the closing tag (">").
                    int endCharIndex = this.Tokenizer.Source.IndexOfAny("<>".ToCharArray(), this.Tokenizer.Position + offset);
                    if (endCharIndex > -1)
                    {
                        if (this.Tokenizer.Source.Substring(endCharIndex, 1).IndexOfAny("<".ToCharArray()) == -1)
                        {
                            this.ScannerResult.Add(new Occurrence(endCharIndex, 1, this.xmlSpecialCharNode));
                            this.OnMatch(new MatchEventArgs(this.Tokenizer.Position, endCharIndex + 1, ns, tag, null, MatchType.EndTag));
                        }
                        this.Tokenizer.MoveTo(endCharIndex);
                    }
                    else
                        this.Tokenizer.MoveTo(this.Tokenizer.Position + offset);
                }
                // Match comments.
                else if ((this.Tokenizer.Position + 4 <= this.Tokenizer.Source.Length) && this.Tokenizer.GetNextTokens(4) == "<!--")
                {
                    int endCharIndex = this.Tokenizer.Source.IndexOf("->", this.Tokenizer.Position + 4);
                    if (endCharIndex == -1)
                        endCharIndex = this.Tokenizer.Source.Length;
                    else
                        endCharIndex += 2;
                    this.ScannerResult.Add(new Occurrence(this.Tokenizer.Position, endCharIndex - this.Tokenizer.Position, this.xmlCommentNode));
                    this.Tokenizer.MoveTo(endCharIndex - 1);
                }
                // Match start identifier.
                else if ((this.Tokenizer.Position + 2 <= this.Tokenizer.Source.Length) && this.Tokenizer.GetNextTokens(2) == "<?")
                {
                    this.ScannerResult.Add(new Occurrence(this.Tokenizer.Position, 2, this.xmlSpecialCharNode));
                    this.OnMatch(new MatchEventArgs(this.Tokenizer.Position, 2, null, null, null, MatchType.StartIdentifier));
                    this.Tokenizer.MoveTo(this.Tokenizer.Position + 1);
                    this.identifierEnabled = true;
                }
                // Match end identifier.
                else if ((this.Tokenizer.Position + 2 <= this.Tokenizer.Source.Length) && this.Tokenizer.GetNextTokens(2) == "?>")
                {
                    this.ScannerResult.Add(new Occurrence(this.Tokenizer.Position, 2, this.xmlSpecialCharNode));
                    this.OnMatch(new MatchEventArgs(this.Tokenizer.Position, 2, null, null, null, MatchType.EndIdentifier));
                    this.Tokenizer.MoveTo(this.Tokenizer.Position + 1);
                    this.identifierEnabled = false;
                }
                // Match attributes.
                else if (this.identifierEnabled)
                {
                    // NOTE: Fix this.
                    /*int length = this.MatchAttributes(1, new Dictionary<string,string>());
                    if (length > -1)
                        this.Tokenizer.MoveTo(this.Tokenizer.Position + length);*/
                }
                // Match either a starting tag, such as "<asp:Label>", or a single tag, such as "<asp:Label />".
                else if ((this.Tokenizer.Position + 1 <= this.Tokenizer.Source.Length) && this.Tokenizer.GetNextTokens(1) == "<")
                {
                    this.ScannerResult.Add(new Occurrence(this.Tokenizer.Position, 1, this.xmlSpecialCharNode));

                    // Match [special chars/namespace/]tag.
                    int offset = 1;
                    string ns = this.MatchNamespace(offset);
                    if (ns.Length > 0)
                        offset += ns.Length + 1;

                    string tag = this.MatchTag(offset);
                    if (tag.Length > 0)
                        offset += tag.Length;

                    // Match attributes.
                    Dictionary<string, string> attributes = new Dictionary<string, string>();
                    if (tag.Length > 0)
                    {
                        int attributesLength = this.MatchAttributes(offset, attributes);
                        if (attributesLength > -1)
                            offset += attributesLength;
                    }

                    // Match end character(s) of the start/single tag, such as ">" or "/>".
                    int endCharIndex = this.Tokenizer.Source.IndexOfAny("\0<>".ToCharArray(), this.Tokenizer.Position + offset);
                    if (endCharIndex > -1)
                    {
                        if (this.Tokenizer.Source.Substring(endCharIndex, 1).IndexOfAny("\0<".ToCharArray()) == -1)
                        {
                            if (this.Tokenizer.Source[endCharIndex - 1] == '/')
                            {
                                this.ScannerResult.Add(new Occurrence(endCharIndex - 1, 2, this.xmlSpecialCharNode));
                                this.OnMatch(new MatchEventArgs(this.Tokenizer.Position, endCharIndex + 1, ns, tag, attributes, MatchType.StandaloneTag));
                            }
                            else
                            {
                                this.ScannerResult.Add(new Occurrence(endCharIndex, 1, this.xmlSpecialCharNode));
                                this.OnMatch(new MatchEventArgs(this.Tokenizer.Position, endCharIndex + 1, ns, tag, attributes, MatchType.StartTag));
                            }
                        }
                        this.Tokenizer.MoveTo(endCharIndex - 1);
                    }
                    else
                        this.Tokenizer.MoveTo(this.Tokenizer.Position + offset);
                }

                if (!isMatch)
                {
                    if (this.Child != null)
                    {
                        this.Child.Scan(token);
                    }
                }
            }
        }

        /// <summary>
        /// Raises the <see cref="Wilco.CodeHilighter.Engine.SyntaxHighlighting.XmlScanner.Match"/> event.
        /// </summary>
        /// <param name="e">An <see cref="System.EventArgs"/> object that contains the event data.</param>
        protected void OnMatch(MatchEventArgs e)
        {
            if (this.Match != null)
                this.Match(this, e);
        }

        /// <summary>
        /// Creates a steting.
        /// </summary>
        /// <param name="document">The document which will contain the setting.</param>
        /// <param name="name">The name of the setting.</param>
        /// <param name="value">The value of the setting.</param>
        /// <returns>The <see cref="System.Xml.XmlElement"/> which represents the setting.</returns>
        private XElement CreateSetting(string name, string value)
        {
            return new XElement("setting", value, new XAttribute("name", name));
        }

        /// <summary>
        /// Loads the information for a node.
        /// </summary>
        /// <param name="element">The <see cref="System.Xml.XmlElement"/> which contains the information about the node.</param>
        /// <param name="node">The <see cref="Wilco.CodeHilighter.Engine.SyntaxHighlighting.INode"/> implementation class for which the information will be set.</param>
        /// <param name="name">The name of the node.</param>
        private void LoadNode(XElement element, INode node, string name)
        {
            XElement nodeElement = (XElement)Helper.GetElemementByAttributeName(element,"nodes","node","name",name);
            node.BackColor = ColorTranslator.FromHtml(Helper.GetAttributeValueFromElement(element,"settings","setting", "name", "BackColor"));
            node.ForeColor = ColorTranslator.FromHtml(Helper.GetAttributeValueFromElement(element, "settings", "setting", "name", "ForeColor"));
            node.Font = Helper.GetAttributeValueFromElement(element,"settings","setting","name","Font");
            node.NavigateUrl = Helper.GetAttributeValueFromElement(element,"settings","setting","name","NavigateUrl");
        }

        /// <summary>
        /// Matches an attributes.
        /// </summary>
        /// <param name="offset">The offset from which the search can begin.</param>
        /// <param name="attributes">The matched attributes.</param>
        /// <returns>The length of the matched attribute value.</returns>
        private int MatchAttributes(int offset, Dictionary<string, string> attributes)
        {
            // NOTE: This method should be reimplemented. The regexp won't work properly when an attribute without a value is entered, e.g.: <?xml a="b"?>
            int lastCharIndex = this.Tokenizer.Source.Length;

            // Get end char of the tag (">").
            bool inString = false;
            for (int i = this.Tokenizer.Position + offset; i < this.Tokenizer.Source.Length; i++)
            {
                if (this.Tokenizer.Source[i] == '\"' || this.Tokenizer.Source[i] == '\'')
                    inString = !inString;
                else if (!inString && this.Tokenizer.Source[i] == '>')
                {
                    lastCharIndex = i;
                    if (this.Tokenizer.Source[i - 1] == '/')
                        lastCharIndex--;
                    break;
                }
            }

            MatchCollection matches = Regex.Matches(this.Tokenizer.Source.Substring(this.Tokenizer.Position + offset, lastCharIndex - (this.Tokenizer.Position + offset)),
                @"(
                    (?'name'\w+) \s* (?'attGroup' = \s*
                    (?'value' [^\s""'/\?>]+ | ""[^""]*"" | '[^']*')){0,1}
                    \s*
                )*",
                RegexOptions.ExplicitCapture|RegexOptions.IgnorePatternWhitespace);

            int length = -1;
            for (int i = 0; i < matches.Count; i++)
            {
                for (int j = 0; j < matches[i].Groups["name"].Captures.Count; j++)
                {
                    if (matches[i].Groups["name"].Captures[j].Length > 0)
                    {
                        this.ScannerResult.Add(new Occurrence(this.Tokenizer.Position + offset + matches[i].Groups["name"].Captures[j].Index, matches[i].Groups["name"].Captures[j].Length, this.xmlAttributeNameNode));

                        if (j < matches[i].Groups["value"].Captures.Count && matches[i].Groups["value"].Captures[j].Length > 0)
                        {
                            this.ScannerResult.Add(new Occurrence(this.Tokenizer.Position + offset + matches[i].Groups["value"].Captures[j].Index, matches[i].Groups["value"].Captures[j].Length, this.xmlAttributeValueNode));
                            length = matches[i].Groups["value"].Captures[j].Index + matches[i].Groups["value"].Captures[j].Length;
                        }
                        else
                            length = matches[i].Groups["name"].Captures[j].Index + matches[i].Groups["name"].Captures[j].Length;

                        if (matches[i].Groups["name"].Captures[j].Length > 0 && j < matches[i].Groups["value"].Captures.Count && matches[i].Groups["value"].Captures[j].Length > 0)
                        {
                string name = matches[i].Groups["name"].Captures[j].Value;
                            string value;
                            if(!attributes.TryGetValue(name, out value))
                                attributes.Add(name, value);
            }
                    }
                }
            }

            return length;
        }

        /// <summary>
        /// Matches a namespace.
        /// </summary>
        /// <param name="offset">The offset from which the search can begin.</param>
        /// <returns>The matched namespace.</returns>
        private string MatchNamespace(int offset)
        {
            if ((this.Tokenizer.Position + offset) < this.Tokenizer.Source.Length && !char.IsNumber(this.Tokenizer.Source, this.Tokenizer.Position + offset))
            {
                for (int i = this.Tokenizer.Position + offset; i < this.Tokenizer.Source.Length; i++)
                {
                    if (!char.IsLetter(this.Tokenizer.Source, i))
                    {
                        if (this.Tokenizer.Source[i] == ':')
                        {
                            int length = i - (this.Tokenizer.Position + offset);
                            if (length > 0)
                                this.ScannerResult.Add(new Occurrence(this.Tokenizer.Position + offset, length, this.xmlNamespaceNode));
                            return this.Tokenizer.Source.Substring(this.Tokenizer.Position + offset, length);
                        }
                        else
                            return String.Empty;
                    }
                }
            }
            return String.Empty;
        }

        /// <summary>
        /// Matches a tag.
        /// </summary>
        /// <param name="offset">The offset from which the search can begin.</param>
        /// <returns>The matched tag.</returns>
        private string MatchTag(int offset)
        {
            if ((this.Tokenizer.Position + offset) < this.Tokenizer.Source.Length && !char.IsNumber(this.Tokenizer.Source, this.Tokenizer.Position + offset))
            {
                for (int i = this.Tokenizer.Position + offset; i < this.Tokenizer.Source.Length; i++)
                {
                    char ch = this.Tokenizer.Source[i];
                    if (ch != ':' && !char.IsLetterOrDigit(ch))
                    {
                        int length = i - (this.Tokenizer.Position + offset);
                        if (length > 0)
                            this.ScannerResult.Add(new Occurrence(this.Tokenizer.Position + offset, length, this.xmlTagNode));
                        return this.Tokenizer.Source.Substring(this.Tokenizer.Position + offset, length);
                    }
                }
                this.ScannerResult.Add(new Occurrence(this.Tokenizer.Position + offset, this.Tokenizer.Source.Length - (this.Tokenizer.Position + offset), this.xmlTagNode));
                return this.Tokenizer.Source.Substring(this.Tokenizer.Position + offset, this.Tokenizer.Source.Length - (this.Tokenizer.Position + offset));
            }
            return String.Empty;
        }

        /// <summary>
        /// Stores a node.
        /// </summary>
        /// <param name="document">The document which will contain the settings for this node.</param>
        /// <param name="nodeRootElement">The element which will hold the node's settings.</param>
        /// <param name="node">The node which should be represented.</param>
        /// <param name="name">The name of the node.</param>
        private void StoreNode(XElement nodeRootElement, INode node, string name)
        {
            XElement nodeElement = new XElement("node", new XAttribute("name", name));
            nodeRootElement.Add(nodeElement);

            XElement settingRootElement = new XElement("settings");
            nodeElement.Add(settingRootElement);

            settingRootElement.Add(this.CreateSetting("BackColor", ColorTranslator.ToHtml(node.BackColor)));
            settingRootElement.Add(this.CreateSetting("ForeColor", ColorTranslator.ToHtml(node.ForeColor)));
            settingRootElement.Add(this.CreateSetting("ForeColor", node.Font));
            settingRootElement.Add(this.CreateSetting("NavigateUrl", node.NavigateUrl));
        }

        #endregion Methods
    }
}